# @Version        : 1.0
# @Update Time    : 2025/3/29 23:51
# @File           : logging_route.py
# @IDE            : PyCharm
# @Desc           : 文件描述信息
from typing import Callable
import time
import os
import uuid
import json
from user_agents import parse
from urllib.parse import parse_qs
from datetime import datetime, timezone
from fastapi.routing import APIRoute, Request, APIRouter, Response, HTTPException
from src.log import logger


class LoggingRoute(APIRoute):
    def get_route_handler(self):
        original_handler = super().get_route_handler()

        async def custom_handler(request: Request):
            # 打印请求信息
            logger.info(
                f"----> {request.method} {request.url.path} {request.client.host}"
            )
            if request.query_params:
                logger.info(f"----> Query Params: {request.query_params}")

            if "application/json" in request.headers.get("Content-Type", ""):
                try:
                    # starlette 中间件中不能读取请求数据，否则会进入循环等待 需要特殊处理或者换APIRoute实现
                    # 读取Body并记录
                    body = await request.body()
                    logger.info(f"----> Body: {body.decode()}")
                except Exception as e:
                    logger.warning(f"Failed to parse JSON body: {e}")
            return await original_handler(request)

        return custom_handler


class BaseAPIRouter(APIRouter):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # 开启api请求日志信息
        self.route_class = LoggingRoute


# === 脱敏/截断配置（支持通过环境变量调整）===
# 单个字符串最大保留长度（超出会截断）
LOG_STRING_MAX = int(os.getenv("LOG_STRING_MAX", "256"))
# 每个数组最多保留的元素个数
LOG_LIST_MAX = int(os.getenv("LOG_LIST_MAX", "50"))
# JSON 递归处理的最大深度（超过深度的值以占位符代替）
LOG_BODY_MAX_DEPTH = int(os.getenv("LOG_BODY_MAX_DEPTH", "8"))

# 需要脱敏的敏感字段（大小写不敏感）
SENSITIVE_KEYS = {
    "password",
    "passwd",
    "passphrase",
    "secret",
    "client_secret",
    "private_key",
    "authorization",
    "auth",
    "token",
    "access_token",
    "refresh_token",
    "id_token",
    "api_key",
    "apikey",
    "api-key",
    "x-api-key",
    "credit_card",
    "card_number",
    "cvv",
    "ssn",
    "cookie",
}
SENSITIVE_KEY_SUBSTR = (
    "password",
    "passwd",
    "secret",
    "token",
    "key",
    "authorization",
    "auth",
    "cookie",
    "card",
    "cvv",
    "ssn",
)


def _truncate_str(s: str, limit: int = LOG_STRING_MAX) -> str:
    if s is None:
        return ""
    if not isinstance(s, str):
        s = str(s)
    return s if len(s) <= limit else s[:limit] + "...(truncated)"


def _is_sensitive(key: str) -> bool:
    k = (key or "").lower()
    return k in SENSITIVE_KEYS or any(sub in k for sub in SENSITIVE_KEY_SUBSTR)


def _redact_and_truncate(obj, depth: int = 0):
    """对 JSON 结构做字段脱敏、字符串/数组截断、递归深度限制。"""
    if depth >= LOG_BODY_MAX_DEPTH:
        return "...(max_depth)"
    if isinstance(obj, dict):
        out = {}
        for k, v in obj.items():
            if _is_sensitive(str(k)):
                out[k] = "***"
            else:
                out[k] = _redact_and_truncate(v, depth + 1)
        return out
    if isinstance(obj, list):
        trimmed = obj[:LOG_LIST_MAX]
        return [_redact_and_truncate(v, depth + 1) for v in trimmed]
    if isinstance(obj, str):
        return _truncate_str(obj)
    if isinstance(obj, (int, float, bool)) or obj is None:
        return obj
    # 其他类型（如 bytes/自定义对象）转为字符串后截断
    return _truncate_str(obj)


def _sanitize_query(q: dict) -> dict:
    """对 query 参数做脱敏与字符串截断。q 形如 {key: [v1, v2]}。"""
    out = {}
    for k, vals in (q or {}).items():
        if _is_sensitive(k):
            out[k] = ["***"]
        else:
            out[k] = [_truncate_str(v) for v in vals]
    return out


class LoggerRoute(APIRoute):
    def get_route_handler(self) -> Callable:
        original_route_handler = super().get_route_handler()

        async def custom_route_handler(request: Request) -> Response:
            try:
                header = dict(request.headers)
                # 优先使用 X-Request-ID，回退到 uuid 头或新生成
                uuid_str = (
                    header.get("x-request-id")
                    or header.get("uuid")
                    or str(uuid.uuid4())
                )

                user_agent = parse(request.headers.get("user-agent", ""))

                browser = user_agent.browser.version or []
                if len(browser) >= 2:
                    browser_major, browser_minor = browser[0], browser[1]
                else:
                    browser_major, browser_minor = 0, 0

                user_os = user_agent.os.version or []
                if len(user_os) >= 2:
                    os_major, os_minor = user_os[0], user_os[1]
                else:
                    os_major, os_minor = 0, 0

                # Request json
                body_bytes = await request.body()
                ctype = (request.headers.get("content-type") or "").lower()
                if body_bytes and "application/json" in ctype:
                    try:
                        body = json.loads(body_bytes)
                    except Exception:
                        body = None  # 不可解析时不抛错，避免影响业务
                else:
                    body = ""

                # 生成 query，并做脱敏/截断
                raw_query = parse_qs(str(request.query_params))
                safe_query = _sanitize_query(raw_query)

                # 对 JSON 请求体做脱敏与截断（非 JSON 的 body 仍为空字符串）
                if body not in ("", None):
                    body = _redact_and_truncate(body)

                # 尽量用真实读取的 body 字节长度
                req_length = request.headers.get("content-length")
                if body_bytes:
                    try:
                        req_length = str(len(body_bytes))
                    except Exception:
                        pass

                request_json = {
                    "type": "request",
                    "uuid": uuid_str,
                    "env": os.environ.get("ENV"),
                    "region": os.environ.get("REGION"),
                    "name": os.environ.get("NAME"),
                    "method": request.method,
                    "useragent": {
                        "family": user_agent.browser.family,
                        "major": browser_major,
                        "minor": browser_minor,
                        "patch": user_agent.browser.version_string,
                        "device": {
                            "family": user_agent.device.family,
                            "brand": user_agent.device.brand,
                            "model": user_agent.device.model,
                            "major": "0",
                            "minor": "0",
                            "patch": "0",
                        },
                        "os": {
                            "family": user_agent.os.family,
                            "major": os_major,
                            "minor": os_minor,
                            "patch": user_agent.os.version_string,
                        },
                    },
                    "url": request.url.path,
                    "query": safe_query,
                    "body": body,
                    "length": req_length,
                    "ts": datetime.now(),
                }

                start_time = time.time()
                response = await original_route_handler(request)
                process_time = (time.time() - start_time) * 1000
                formatted_process_time = "{0:.2f}".format(process_time)

                metrics_json = {
                    "type": "metrics",
                    "uuid": uuid_str,
                    "env": os.environ.get("ENV"),
                    "region": os.environ.get("REGION"),
                    "name": os.environ.get("NAME"),
                    "method": request.method,
                    "status_code": response.status_code,
                    "url": request.url.path,
                    "query": safe_query,
                    "length": response.headers.get("content-length"),
                    "latency": formatted_process_time,
                    "ts": datetime.now(),
                }
                if "health" not in request.url.path:
                    print(json.dumps(request_json, indent=4))
                    print(json.dumps(metrics_json, indent=4))

                try:
                    if len(request_json) != 0:
                        url = str(request_json["url"]).replace("/", "")
                        method = request_json["method"]
                        body = str(request_json["body"])
                        uuid_str = request_json["uuid"]
                        print(body)

                except Exception as exc:
                    print(exc)
                    pass

                return response

            except Exception as exc:
                body = await request.body()
                detail = {"errors": str(exc), "body": body.decode("utf-8", "ignore")}
                print(detail)
                # 保持原始异常语义，避免把所有错误都变成 422
                raise

        return custom_route_handler
